<?php

namespace App\Entities;

use App\Contracts\GeneratesUniqueHash;
use App\Contracts\HasIdColumn;
use App\Contracts\RelatesToFiles;
use App\Contracts\SystemComponent;
use App\Entities\Base\AbstractEntity;
use App\Entities\Base\User;
use App\Entities\Base\VulnerabilityHttpData;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Illuminate\Support\Collection;

/**
 * App\Entities\Vulnerability
 *
 * @ORM\Entity(repositoryClass="App\Repositories\VulnerabilityRepository")
 * @ORM\HasLifecycleCallbacks
 */
class Vulnerability extends Base\Vulnerability implements SystemComponent, HasIdColumn, RelatesToFiles, GeneratesUniqueHash
{
    /** Entity Collection name constants */
    const ASSETS   = 'assets';
    const EXPLOITS = 'exploits';
    const FOLDERS  = 'folders';

    /** Severity text constants */
    const SEVERITY_INFORMATION = '通知';
    const SEVERITY_LOW         = '低微';
    const SEVERITY_MEDIUM      = '中危';
    const SEVERITY_HIGH        = '高危';
    const SEVERITY_CRITICAL    = '致命';

    /** Severity score constants */
    const SEVERITY_INFORMATION_DEFAULT_SCORE = 0;
    const SEVERITY_LOW_DEFAULT_SCORE         = 3.9;
    const SEVERITY_MEDIUM_DEFAULT_SCORE      = 6.9;
    const SEVERITY_HIGH_DEFAULT_SCORE        = 9.9;
    const SEVERITY_CRITICAL_DEFAULT_SCORE    = 10;

    /** Thumbnail extension constants */
    const IMAGE_EXTENSION_PNG  = 'png';
    const IMAGE_EXTENSION_JPG  = 'jpg';
    const IMAGE_EXTENSION_JPEG = 'jpeg';
    const IMAGE_EXTENSION_BMP  = 'bmp';
    const IMAGE_EXTENSION_GIF  = 'gif';
    const IMAGE_EXTENSION_SVG  = 'svg';

    /**
     * @ORM\ManyToOne(targetEntity="File", inversedBy="vulnerabilities", cascade={"persist"}, fetch="EAGER")
     * @ORM\JoinColumn(name="`file_id`", referencedColumnName="`id`", onDelete="CASCADE")
     */
    protected $file;

    /**
     * @ORM\ManyToMany(targetEntity="Folder", mappedBy="vulnerabilities")
     */
    protected $folders;

    /**
     * @ORM\ManyToMany(targetEntity="Asset", mappedBy="vulnerabilities")
     */
    protected $assets;

    /**
     * @ORM\ManyToMany(targetEntity="Exploit", inversedBy="vulnerabilities", cascade={"persist"}, fetch="EXTRA_LAZY")
     * @ORM\JoinTable(name="vulnerabilities_exploits",
     *     joinColumns={@ORM\JoinColumn(name="vulnerability_id", referencedColumnName="id", onDelete="CASCADE")},
     *     inverseJoinColumns={@ORM\JoinColumn(name="exploit_id", referencedColumnName="id", onDelete="CASCADE")}
     * )
     */
    protected $exploits;

	/**
	 * @ORM\OneToMany(targetEntity="Comment", mappedBy="vulnerability", cascade={"persist"}, fetch="EAGER")
	 * @ORM\OrderBy({"created_at" = "DESC"})
	 * @ORM\JoinColumn(name="`id`", referencedColumnName="`vulnerability_id`", nullable=false, onDelete="CASCADE")
	 */
	protected $comments;

    /**
     * Vulnerability constructor.
     */
    public function __construct()
    {
        parent::__construct();
        $this->assets   = new ArrayCollection();
        $this->exploits = new ArrayCollection();
        $this->folders  = new ArrayCollection();
    }

    /**
     * @inheritdoc
     *
     * @return User
     */
    public function getUser()
    {
        return $this->file->getUser();
    }

    /**
     * @inheritdoc
     *
     * @param User $user
     * @return $this
     */
    public function setUser(User $user)
    {
        return $this;
    }

    /**
     * @inheritdoc
     *
     * @return Base\File
     */
    public function getParent()
    {
        return $this->file;
    }

    /**
     * Check that that we received a valid severity
     *
     * @param float $severity
     * @return Base\Vulnerability
     */
    public function setSeverity($severity)
    {
        if (is_numeric($severity)) {
            return parent::setSeverity($severity);
        }

        // Check is the score is in the map
        $score = $this->getSeverityTextToScoreMap()->get($severity);
        if (!isset($score)) {
            return $this;
        }

        return parent::setSeverity($score);
    }

    /**
     * Override the parent method to set the $malwareAvailable property at the same time
     *
     * @param string $malware_description
     * @return Base\Vulnerability
     */
    public function setMalwareDescription($malware_description)
    {
        $this->setMalwareAvailable(!empty($malware_description));
        return parent::setMalwareDescription($malware_description);
    }

    /**
     * Override parent method to format the date when an invalid date format is encountered
     *
     * @param \DateTime $published_date_from_scanner
     * @return Base\Vulnerability
     */
    public function setPublishedDateFromScanner($published_date_from_scanner)
    {
        if (empty($this->sanitiseDate($published_date_from_scanner))) {
            return $this;
        }

        return parent::setPublishedDateFromScanner(
            $this->sanitiseDate($published_date_from_scanner)
        );
    }

    /**
     * Override parent method to format the date when an invalid date format is encountered
     *
     * @param \DateTime $modified_date_from_scanner
     * @return Base\Vulnerability
     */
    public function setModifiedDateFromScanner($modified_date_from_scanner)
    {
        if (empty($this->sanitiseDate($modified_date_from_scanner))) {
            return $this;
        }

        return parent::setModifiedDateFromScanner(
            $this->sanitiseDate($modified_date_from_scanner)
        );
    }

    /**
     * Override the parent method to set the inverse relation on the VulnerabilityReferenceCode
     *
     * @param Base\VulnerabilityReferenceCode $vulnerabilityReferenceCode
     * @return Base\Vulnerability
     */
    public function addVulnerabilityReferenceCode(Base\VulnerabilityReferenceCode $vulnerabilityReferenceCode)
    {
        if ($this->getVulnerabilityReferenceCodes()->contains($vulnerabilityReferenceCode)) {
            return $this;
        }

        $vulnerabilityReferenceCode->setVulnerability($this);
        return parent::addVulnerabilityReferenceCode($vulnerabilityReferenceCode);
    }

    /**
     * Override the parent method to set the inverse relation on the VulnerabilityHttpData entity
     *
     * @param VulnerabilityHttpData $vulnerabilityHttpData
     * @return Base\Vulnerability
     */
    public function addVulnerabilityHttpData(VulnerabilityHttpData $vulnerabilityHttpData)
    {
        if ($this->vulnerabilityHttpData->contains($vulnerabilityHttpData)) {
            return $this;
        }

        $vulnerabilityHttpData->setVulnerability($this);
        return parent::addVulnerabilityHttpData($vulnerabilityHttpData);
    }

    /**
     * @param Exploit $exploit
     * @return $this
     */
    public function addExploit(Exploit $exploit)
    {
        if ($this->exploits->contains($exploit)) {
            return $this;
        }

        $relationKey = $exploit->getId() ?? $exploit->getHash();
        $this->exploits[$relationKey] = $exploit;

        return $this;
    }

    /**
     * Get a related exploit by it's hash
     *
     * @param string $hash
     * @return Exploit|null
     */
    public function getExploit(string $hash)
    {
        if (!isset($this->exploits[$hash])) {
            return null;
        }

        return $this->exploits[$hash];
    }

    /**
     * @param Exploit $exploit
     * @return $this
     */
    public function removeExploit(Exploit $exploit)
    {
        $this->exploits->removeElement($exploit);

        return $this;
    }

    /**
     * @return ArrayCollection
     */
    public function getExploits()
    {
        return $this->exploits;
    }

    /**
     * Get a hash value of all the property values whose combination creates a unique key
     *
     * @return string
     */
    public function getHash(): string
    {
        return AbstractEntity::generateUniqueHash($this->getUniqueKeyColumns());
    }

    /**
     * @return Collection
     */
    public function getUniqueKeyColumns(): Collection
    {
        return new Collection([
            parent::ID_FROM_SCANNER => $this->id_from_scanner,
            parent::NAME            => $this->name,
            parent::FILE            => $this->file,
        ]);
    }

    /**
     * Map of Information, Low, Medium, High and Critical severities to the relevant score
     *
     * @return Collection
     */
    public static function getSeverityTextToScoreMap(): Collection
    {
        return collect([
            static::SEVERITY_INFORMATION => static::SEVERITY_INFORMATION_DEFAULT_SCORE,
            static::SEVERITY_LOW         => static::SEVERITY_LOW_DEFAULT_SCORE,
            static::SEVERITY_MEDIUM      => static::SEVERITY_MEDIUM_DEFAULT_SCORE,
            static::SEVERITY_HIGH        => static::SEVERITY_HIGH_DEFAULT_SCORE,
            static::SEVERITY_CRITICAL    => static::SEVERITY_CRITICAL_DEFAULT_SCORE,
        ]);
    }

    /**
     * Get the relevant text associated with the severity score, e.g. Low, Medium, High, Critical
     *
     * @return mixed
     */
    public function getSeverityText()
    {
        // Get the score that best matches this Vulnerability's severity score
        $scoreDefault = $this->getSeverityTextToScoreMap()->first(function ($score) {
            return $this->getSeverity() <= $score;
        }, self::SEVERITY_INFORMATION);

        // Return the key (Text representation) related to the score retrieved above
        return $this->getSeverityTextToScoreMap()->search($scoreDefault, true);
    }

    /**
     * Get the bootstrap CSS class related to the Vulnerabilities severity
     *
     * @return mixed
     */
    public function getBootstrapAlertCssClass()
    {
        return collect([
            self::SEVERITY_INFORMATION => 'info',
            self::SEVERITY_LOW         => 'success',
            self::SEVERITY_MEDIUM      => 'warning',
            self::SEVERITY_HIGH        => 'high',
            self::SEVERITY_CRITICAL    => 'danger',
        ])->get($this->getSeverityText(), ',info');
    }

    /**
     * Add a relationship to a Folder entity
     *
     * @param Folder $folder
     * @return $this
     */
    public function addFolder(Folder $folder)
    {
        $this->folders[$folder->getId()] = $folder;

        return $this;
    }

    /**
     * Remove a relationship to a Folder entity
     *
     * @param Folder $folder
     * @return $this
     */
    public function removeFolder(Folder $folder)
    {
        $this->folders->removeElement($folder);

        return $this;
    }

    /**
     * Get the Folders that contain this Vulnerability
     *
     * @return ArrayCollection
     */
    public function getFolders()
    {
        return $this->folders;
    }

    /**
     * Implement for the RelatesToFiles contract. Just an alias for the parent setFile() method in this case.
     *
     * @param File $file
     * @return Base\Vulnerability
     */
    public function addFile(File $file)
    {
        return parent::setFile($file);
    }

    /**
     * Implement for the RelatesToFiles contract. Cannot orphan this Vulnerability
     * by unsetting the File relationship, so do nothing
     *
     * @param File $file
     * @return $this
     */
    public function removeFile(File $file)
    {
        return $this;
    }

    /**
     * Add an Asset entity to the Collection (many-to-many).
     *
     * @param Asset $asset
     * @return $this
     */
    public function addAsset(Asset $asset)
    {
        $this->assets[$asset->getId()] = $asset;

        return $this;
    }

    /**
     * Remove an Asset entity to the Collection (many-to-many).
     *
     * @param Asset $asset
     * @return Vulnerability
     */
    public function removeAsset(Asset $asset)
    {
        $this->assets->removeElement($asset);

        return $this;
    }

    /**
     * Get the Collection of Asset entities
     *
     * @return ArrayCollection
     */
    public function getAssets()
    {
        return $this->assets;
    }

    /**
	 * Get the comments related to the Vulnerability in descending order. This cannot be done using SQL with Doctrine
     * because comments are set to eager load.
     *
     * @see https://github.com/doctrine/doctrine2/issues/4256
     * @see https://github.com/doctrine/doctrine2/pull/6155
	 *
	 * @return Collection
	 */
    public function getComments()
    {
    	return collect(parent::getComments()->toArray())
		    ->sortByDesc(function ($comment) {
		    	/** @var Comment $comment */
		    	return $comment->getCreatedAt();
		    });
    }

    /**
     * Get the storage path for Proof of Concept vulnerability exploit thumbnails
     *
     * @return string
     */
    public static function getThumbnailStoragePath(): string
    {
        return storage_path('app/public/poc') . DIRECTORY_SEPARATOR;
    }

    /**
     * Get a Collection of accepted thumbnail image extensions
     *
     * @return Collection
     */
    public static function getAcceptedThumbnailExtensions(): Collection
    {
        return collect([
            static::IMAGE_EXTENSION_PNG,
            static::IMAGE_EXTENSION_JPG,
            static::IMAGE_EXTENSION_JPEG,
            static::IMAGE_EXTENSION_BMP,
            static::IMAGE_EXTENSION_GIF,
            static::IMAGE_EXTENSION_SVG,
        ]);
    }

    /**
     * Check if a file extension is acceptable as a thumbnail upload
     *
     * @param string $extension
     * @return bool
     */
    public static function isAcceptedThumbnailExtension(string $extension): bool
    {
        return !empty($extension) && static::getAcceptedThumbnailExtensions()->contains(strtolower($extension));
    }

    /**
     * Convenience method used for iterating over request variables and using the relevant getter within the iteration
     *
     * @return Collection
     */
    public function getThumbnailPropertyGetterMap(): Collection
    {
        return collect([
            'thumbnail_1' => 'getThumbnail1',
            'thumbnail_2' => 'getThumbnail2',
            'thumbnail_3' => 'getThumbnail3',
        ]);
    }

    /**
     * Convenience method used for iterating over request variables and using the relevant setter within the iteration
     *
     * @return Collection
     */
    public function getThumbnailPropertySetterMap(): Collection
    {
        return collect([
            'thumbnail_1' => 'setThumbnail1',
            'thumbnail_2' => 'setThumbnail2',
            'thumbnail_3' => 'setThumbnail3',
        ]);
    }
}